/*
 * Copyright 2013 - 2020 Outworkers Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.outworkers.phantom.builder.query

import com.outworkers.phantom.CassandraTable
import com.outworkers.phantom.builder.ops.DropColumn
import com.outworkers.phantom.builder.primitives.Primitive
import com.outworkers.phantom.builder.query.engine.CQLQuery
import com.outworkers.phantom.builder.query.execution.ExecutableCqlQuery
import com.outworkers.phantom.builder.query.options.{TablePropertyClause, WithBound, WithChainned, WithUnchainned}
import com.outworkers.phantom.builder.{ConsistencyBound, QueryBuilder, Unspecified}
import com.outworkers.phantom.column.AbstractColumn
import com.outworkers.phantom.connectors.KeySpace

case class AlterQuery[
  Table <: CassandraTable[Table, _],
  Record,
  Status <: ConsistencyBound,
  Chain <: WithBound
](
  table: Table,
  init: CQLQuery,
  options: QueryOptions = QueryOptions.empty,
  alterPart: AlterPart = AlterPart.empty,
  addPart: AddPart = AddPart.empty,
  dropPart: DropPart = DropPart.empty,
  withPart: WithPart = WithPart.empty,
  renamePart: RenamePart = RenamePart.empty
) extends RootQuery[Table, Record, Status] {

  val qb: CQLQuery = (alterPart merge addPart merge dropPart merge renamePart merge withPart) build init

  final def add(column: String, columnType: String, static: Boolean = false): AlterQuery[Table, Record, Status, Chain] = {
    val query = if (static) {
      QueryBuilder.Alter.addStatic(column, columnType)
    } else {
      QueryBuilder.Alter.add(column, columnType)
    }

    copy(addPart = addPart append query)
  }

  final def add(definition: CQLQuery): AlterQuery[Table, Record, Status, Chain] = {
    copy (addPart = addPart append definition)
  }

  final def alter[RR](columnSelect: Table => AbstractColumn[RR], newType: String): AlterQuery[Table, Record, Status, Chain] = {
    copy(
      alterPart = alterPart append QueryBuilder.Alter.alterType(columnSelect(table).name, newType)
    )
  }

  final def rename[RR](select: Table => AbstractColumn[RR], newName: String) : AlterQuery[Table, Record, Status, Chain] = {
    copy(
      renamePart = renamePart append QueryBuilder.Alter.rename(select(table).name, newName)
    )
  }

  /**
    * Creates an ALTER drop query to drop the column from the schema definition.
    * It will produce the following type of queries, with the CQL serialization on the right hand side:
    *
    * {{{
    *   MyTable.alter.drop(_.mycolumn) => ALTER TABLE MyTable DROP myColumn
    * }}}
    *
    * @param columnSelect A column selector higher order function derived from a table.
    * @tparam RR The underlying type of the AbstractColumn.
    * @return A new alter query with the underlying builder containing a DROP clause.
    */
  final def drop[RR](columnSelect: Table => DropColumn[RR]): AlterQuery[Table, Record, Status, Chain] = {
    drop(columnSelect(table).column.name)
  }

  /**
    * Creates an ALTER DROP query that drops an entire table.
    * This is equivalent to table truncation followed by table removal from the keyspace metadata.
    * This action is irreversible and you should exercise caution is using it.
    *
    * @param keySpace The implicit keyspace definition to use.
    * @return An alter query with a DROP TABLE instruction encoded in the query string.
    */
  final def drop()(implicit keySpace: KeySpace): AlterQuery[Table, Record, Status, Chain] = {
    new AlterQuery(table, QueryBuilder.Alter.dropTable(table.tableName, keySpace.name), options)
  }

  final def dropIfExists()(implicit keySpace: KeySpace): AlterQuery[Table, Record, Status, Chain] = {
    new AlterQuery(table, QueryBuilder.Alter.dropTableIfExist(table.tableName, keySpace.name), options)
  }

  /**
    * Creates an ALTER drop query to drop the column from the schema definition.
    * It will produce the following type of queries, with the CQL serialization on the right hand side:
    *
    * {{{
    *   MyTable.alter.drop(_.mycolumn) => ALTER TABLE MyTable DROP myColumn
    * }}}
    *
    * This is used mainly during the autodiffing of schemas, where column selectors are not available
    * and we only deal with plain string diffs between table metadata collections.
    *
    * @param column The string name of the column to drop.
    * @return A new alter query with the underlying builder containing a DROP clause.
    */
  final def drop(column: String): AlterQuery[Table, Record, Status, Chain] = {
    copy(dropPart = dropPart append CQLQuery(column))
  }

  @deprecated("Use option instead", "2.0.0")
  final def `with`(clause: TablePropertyClause)(
    implicit ev: Chain =:= WithUnchainned
  ): AlterQuery[Table, Record, Status, WithChainned] = option(clause)

  final def option(clause: TablePropertyClause)(
    implicit ev: Chain =:= WithUnchainned
  ): AlterQuery[Table, Record, Status, WithChainned] = {
    copy(withPart = withPart append clause.qb)
  }

  final def and(clause: TablePropertyClause)(implicit ev: Chain =:= WithChainned): AlterQuery[Table, Record, Status, WithChainned] = {
    copy(withPart = withPart append clause.qb)
  }

  override def executableQuery: ExecutableCqlQuery = ExecutableCqlQuery(qb, options, Nil)
}

object AlterQuery {

  /**
    * An alias for the default AlterQuery type, available to public accessors.
    * Allows for simple API usage of type member where a method can pre-set all the phantom types
    * of all alter query in the background, without leaking them to public APIs.
    *
    * {{{
    *   def alter[T <: CassandraTable[T, _], R]: AlterQuery.Default[T, R]
    * }}}
    *
    * @tparam T The type of the underlying Cassandra table.
    * @tparam R The type of the record stored in the Cassandra table.
    */
  type Default[T <: CassandraTable[T, _], R] = AlterQuery[T, R, Unspecified, WithUnchainned]

  /**
    * The root builder of an ALTER query.
    * This will initialise the alter builder chain and provide the initial {{{ ALTER $TABLENAME }}} query.
    * @param table The table to alter.
    * @tparam T The type of the table.
    * @tparam R The record held in the table.
    * @return A raw ALTER query, without any further options set on it.
    */
  def apply[T <: CassandraTable[T, _], R](table: T)(
    implicit keySpace: KeySpace
  ): AlterQuery.Default[T, R] = {
    new AlterQuery[T, R, Unspecified, WithUnchainned](
      table,
      QueryBuilder.Alter.alter(QueryBuilder.keyspace(keySpace.name, table.tableName).queryString)
    )
  }

  def alterType[
    T <: CassandraTable[T, _],
    R,
    NewType
  ](table: T, select: T => AbstractColumn[R], newType: Primitive[NewType])(
    implicit keySpace: KeySpace
  ): AlterQuery.Default[T, R] = {

    new AlterQuery(
      table,
      QueryBuilder.Alter.alter(QueryBuilder.keyspace(keySpace.name, table.tableName).queryString),
      QueryOptions.empty,
      AlterPart.empty append QueryBuilder.Alter.alterType(select(table).name, newType.dataType)
    )
  }

  def alterName[
    T <: CassandraTable[T, _],
    R
  ](table: T, select: T => AbstractColumn[R], newName: String)(
    implicit keySpace: KeySpace
  ): AlterQuery.Default[T, R] = {

    val qb = QueryBuilder.Alter.alter(
      QueryBuilder.keyspace(keySpace.name, table.tableName).queryString
    )

    new AlterQuery(
      table,
      qb,
      QueryOptions.empty,
      AlterPart.empty append QueryBuilder.Alter.rename(select(table).name, newName)
    )
  }
}